package io.bloom.search;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.*;
import org.apache.lucene.search.*;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;
import org.wltea.analyzer.lucene.IKAnalyzer;

import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;


public class IndexTemplate {

    private final static Log log = LogFactory.getLog(IndexTemplate.class);
    private final static IKAnalyzer analyzer = new IKAnalyzer();
    private final static int MAX_COUNT = 100000;
    private final static int NUM_100M = 104857600;
    private String indexPath;


    public static IndexTemplate init(String idx_path) throws IOException {
        IndexTemplate template = new IndexTemplate();
        idx_path = FilenameUtils.normalize(idx_path);
        File file = new File(idx_path);
        if (!file.exists() || !file.isDirectory()) {
            throw new FileNotFoundException(idx_path);
        }
        if (!idx_path.endsWith(File.separator)) {
            idx_path += File.separator;
        }
        template.indexPath = idx_path;

        return template;
    }

    private static List<Searchable> toSearchables(IndexSearcher searcher, TopDocs topFieldDocs) {

        ScoreDoc[] scoreDocs = topFieldDocs.scoreDocs;
        List<Searchable> list = new ArrayList<>();
        for (ScoreDoc scoreDoc : scoreDocs) {
            try {
                Document doc = searcher.doc(scoreDoc.doc);
                Searchable obj = newSearchable(doc);
                list.add(obj);
            } catch (IOException e) {
                log.error("error to convert the doc", e);
            }
        }
        return list;
    }

    private static Searchable newSearchable(Document document) {
        String instanceClass = document.get(Searchable.DOC_CLASSNAME);
        try {
            Searchable searchable = (Searchable) Class.forName(instanceClass).newInstance();
            searchable.fromDocument(document);
            return searchable;
        } catch (Exception e) {
            log.warn("can not instance " + instanceClass, e);
            return null;
        }
    }

    public int execUpdate(Collection<Searchable> searchables, Class clz, IndexWriterConfig.OpenMode openMode, boolean optimize, IndexHandler indexHandler) throws IOException {
        if (searchables == null) {
            return -1;
        }

        FSDirectory dir = openFsDir(clz);
        IndexWriter writer = getWriter(dir, openMode);
        try {
            int i = 0;
            for (Searchable searchable : searchables) {
                if (indexHandler.handle(searchable, writer) > 0) {
                    i++;
                }
            }

            if (optimize) {
                writer.forceMerge(10);
            }

            writer.commit();
            return i;
        } finally {
            close(writer);
            close(dir);
        }
    }

    public <T> T execQuery(Collection<Class> classes, Query query, Filter filter, Sort sort, QueryHandler<T> handler) throws IOException {
        List<IndexReader> readers = new ArrayList<IndexReader>();
        List<FSDirectory> dirs = new ArrayList<FSDirectory>();
        try {
            for (Class clz : classes) {
                FSDirectory dir = openFsDir(clz);
                dirs.add(dir);
                readers.add(DirectoryReader.open(dir));
            }
            IndexSearcher searcher = new IndexSearcher(new MultiReader(readers.toArray(new IndexReader[0]), true));
            return handler.handle(searcher, query, filter, sort);
        } finally {
            for (IndexReader reader : readers) {
                close(reader);
            }
            for (FSDirectory dir : dirs) {
                close(dir);
            }
        }
    }

    public List<Searchable> find(Collection<Class> classes, Query query, Filter filter, Sort sort) throws IOException {
        return execQuery(classes, query, filter, sort, new QueryHandler<List<Searchable>>() {
            @Override
            public List<Searchable> handle(IndexSearcher searcher, Query query, Filter filter, Sort sort) {
                if (sort == null) {
                    sort = Sort.RELEVANCE;
                }

                try {
                    TopDocs topFieldDocs = searcher.search(query, filter, MAX_COUNT, sort);
                    return toSearchables(searcher, topFieldDocs);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }

    public List<Searchable> find(Class clz, Query query, Filter filter, Sort sort) throws IOException {
        return execQuery(Collections.singleton(clz), query, filter, sort, new QueryHandler<List<Searchable>>() {
            @Override
            public List<Searchable> handle(IndexSearcher searcher, Query query, Filter filter, Sort sort) {
                if (sort == null) {
                    sort = Sort.RELEVANCE;
                }

                try {
                    TopDocs topFieldDocs = searcher.search(query, filter, MAX_COUNT, sort);
                    return toSearchables(searcher, topFieldDocs);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }

    public int count(Collection<Class> classes, Query query, Filter filter) throws IOException {
        return execQuery(classes, query, filter, null, new QueryHandler<Integer>() {
            @Override
            public Integer handle(IndexSearcher searcher, Query query, Filter filter, Sort sort) {
                try {
                    TotalHitCountCollector thcc = new TotalHitCountCollector();
                    if (filter != null) {
                        searcher.search(query, filter, thcc);
                    } else {
                        searcher.search(query, thcc);
                    }
                    return Math.min(MAX_COUNT, thcc.getTotalHits());
                } catch (IOException e) {
                    log.error("Can not find via query: " + query, e);
                    return -1;
                }
            }
        });
    }

    private List<Searchable> find(IndexSearcher searcher, Query query, Filter filter, Sort sort) {

        if (sort == null) {
            sort = Sort.RELEVANCE;
        }

        try {
            TopDocs topFieldDocs = searcher.search(query, filter, MAX_COUNT, sort);
            return toSearchables(searcher, topFieldDocs);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public int update(Collection<Searchable> objects, Class clz) throws IOException {
        return execUpdate(objects, clz, IndexWriterConfig.OpenMode.CREATE_OR_APPEND, false, new IndexHandler() {
            @Override
            public int handle(Searchable object, IndexWriter writer) throws IOException {
                writer.updateDocument(idTerm(object.id()), object.toDocument());
                return 1;
            }
        });
    }

    public int optimize(Class clz) throws IOException {
        return execUpdate(null, clz, IndexWriterConfig.OpenMode.CREATE_OR_APPEND, true, new IndexHandler() {
            @Override
            public int handle(Searchable object, IndexWriter writer) throws IOException {
                writer.updateDocument(idTerm(object.id()), object.toDocument());
                return 1;
            }
        });
    }

    public int delete(Collection<Searchable> objects, Class clz) throws IOException {
        return execUpdate(objects, clz, null, false, new IndexHandler() {
            @Override
            public int handle(Searchable object, IndexWriter writer) throws IOException {
                writer.deleteDocuments(idTerm(object.id()));
                return 1;
            }
        });
    }

    public int create(Collection<Searchable> objects, Class clz) throws IOException {
        return execUpdate(objects, clz, IndexWriterConfig.OpenMode.CREATE_OR_APPEND, false, new IndexHandler() {
            @Override
            public int handle(Searchable object, IndexWriter writer) throws IOException {
                writer.addDocument(object.toDocument());
                return 1;
            }
        });
    }

    public Term idTerm(String value) {
        return new Term(Searchable.DOC_ID, value);
    }

    public Term idTerm(Number value) {
        return idTerm(String.valueOf(value));
    }

    private void close(Closeable closeable) throws IOException {
        if (closeable != null) {
            closeable.close();
            closeable = null;
        }
    }

    private File indexDir(Class<?> clz) {
        return new File(indexPath + clz.getSimpleName());
    }

    private FSDirectory openFsDir(Class<?> clz) throws IOException {
        return FSDirectory.open(indexDir(clz));
    }

    private IndexWriter getWriter(FSDirectory dir, IndexWriterConfig.OpenMode openMode) throws IOException {
        if (dir == null) {
            throw new RuntimeException();
        }

        IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_40, analyzer);
        if (openMode != null) {
            config.setOpenMode(openMode);

        }
        return new IndexWriter(dir, config);
    }

    public interface IndexHandler {

        int handle(Searchable object, IndexWriter writer) throws IOException;
    }

    public static abstract class QueryHandler<T> {

        public abstract T handle(IndexSearcher searcher, Query query, Filter filter, Sort sort) throws IOException;
    }


}
